/*
 * Copyright 2011 Luis Atencio
 * 
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package net.rt.bind;

/**
 * A proxy forces object method calls to occur indirectly through the proxy object, 
 * which acts as a surrogate or delegate for the underlying object being proxied. 
 * Proxy objects are usually declared so that the client objects have no indication 
 * that they have a proxy object instance.
 * 
 * @author Luis Atencio
 */

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.WildcardType;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.activation.UnsupportedDataTypeException;

import net.rt.api.PropertyAccessor;
import net.rt.meta.DefaultValue;
import net.rt.meta.Property;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

public class DynamicProxy implements MethodInterceptor {

	private final Map<String, Object> methodCache;
	private final Map<String, String> propertyMap;

	public DynamicProxy(final Map<String, String> properties) {
		
		propertyMap = properties;
		methodCache = new HashMap<String, Object>();
	}

	/**
	 * Create a new Proxy instance that will act in favor of this interface. This method uses CGLIB to dynamically
	 * generate a proxy implementation for this interface. 
	 * 
	 * @param <T>   Interface class type
	 * @param clazz Interface class
	 * @param properties Properties map.
	 * @return An implementation for this interface with all properties bound to each method
	 */
	@SuppressWarnings("unchecked")
	public static <T> T newInstance(Class<T> clazz,
			Map<String, String> properties) {
		
		try {
			DynamicProxy proxy = new DynamicProxy(properties);
			Enhancer proxyEnhancer = new Enhancer();
			proxyEnhancer.setSuperclass(clazz);
			proxyEnhancer.setCallback(proxy);
			return (T) proxyEnhancer.create();
		} catch (Exception e) {
			throw new CreateBindException(
					"Was not able to synthesize properties into "
							+ clazz.getSimpleName());
		}
	}

	/**
	 * Intercept the method invocation. If the method did not have an annotation specified, 
	 * it will return null.
	 * 
	 * @param surrogate The object it is substituting for.
	 * @param method The actual method object invocation
	 * @param args The argument list
	 * @param methodProxy Proxy for method call. 
	 */
	@Override
	public Object intercept(Object surrogate, Method method, Object[] args,
			MethodProxy methodProxy) throws Throwable {

		// Do not use hashcode to cache use method name
		Property prop = method.getAnnotation(Property.class);
		if (prop == null) {
			// Method has no property annotation
			if(Modifier.isPublic(method.getModifiers())) {
				if(method.getDeclaringClass() == PropertyAccessor.class) {
					
					// This is a provided convenienve method, check method name and execute accordingly
					if(method.getName().equals("getAllPropertyMap")) {
						return propertyMap;
					}
				}
				
				// Invoke method normally and bypass proxy
				return methodProxy.invokeSuper(surrogate, args);
			}
			return null;
		} else {
			
			// Check cache first
			boolean containsKey = methodCache.containsKey(method.getName());
			Object value = methodCache.get(method.getName());

			if (!containsKey && value == null) {

				String key = prop.key();
				String propValue = propertyMap.get(key);
				
				// Choose presentation depending on the type
				value = gePropertyValue(method, propValue);
				
				methodCache.put(method.getName(), value);
				
				return value;
			}
			return value;
		}
	}
	
	
	/**
	 * Figure out the return type of this  method call, supported are (ex. List<T> or String),
	 * where T is a boxed primite. 
	 * 
	 * If there is one string and the return type of the defining interface is a List, 
	 * then it will return a single item List.
	 * 
	 * If a component type cannot be deduced, <i>String</i> will be used by default.
	 * 
	 * @param m   Method that got invoked
	 * @param str The property value to parse
	 * @return The property value in either a list form or a simple string.
	 * @throws NoSuchFieldException 
	 * @throws SecurityException 
	 * @throws ClassNotFoundException 
	 * @throws UnsupportedDataTypeException 
	 */
	private Object gePropertyValue(Method m, String str) 
		throws UnsupportedDataTypeException {
		
		Class<?> returnType = m.getReturnType();
		
		if(str == null || str.length() == 0) {
			
			// Check for a default value
			DefaultValue df = m.getAnnotation(DefaultValue.class);
			if(df != null) {
				str = df.value();
			}
		}
		
		// If the return type is a List, cast the value to the list's
		// runtime parameter type
		if(returnType.getSimpleName().equals(List.class.getSimpleName())) {
			
			if(str == null) {
				return Collections.<String>emptyList();
			}
			
			// Capture runtime type of the list's components
			Class<?> type = extractParameterizedType(m);
			
			if(str.indexOf(',') > 0) {
				
				// Cast each token to the list's parameter type
				String[] rawArr = trim(str.split(","));
				List<Object> temp = new ArrayList<Object>(rawArr.length);
				for(String token : rawArr) {
					temp.add(capturePrimitive(type, token));  // cast is failing, fugure out runtime type maybe by name?
				}
				return temp;
			}
			else {
				return Arrays.asList(new Object[] {type.cast(str)});
			}
		}
		else {
			
			if(str == null) {
				return null;
			}
			return capturePrimitive(returnType, str);
		}
	}
	
	private Class<?> extractParameterizedType(Method m) {

		Type t = m.getGenericReturnType();
		if(!(t instanceof ParameterizedType)) {
			return String.class;
		}		
		else {
			ParameterizedType pt = (ParameterizedType)t;
			for(Type type : pt.getActualTypeArguments()) {
				if (type instanceof Class<?>)
		            return (Class<?>) type;

		        if (type instanceof WildcardType) {
		            WildcardType wildcarded = (WildcardType) type;
		            if (wildcarded.getLowerBounds().length == 0 && Object.class.equals(wildcarded.getUpperBounds()[0]))
		                return String.class;
		        }
			}
		}
		return null;
	}
	
	private Object capturePrimitive(Class<?> returnType, String rawValue) throws UnsupportedDataTypeException {
		
		if(returnType == String.class) {
			return rawValue;
		}
		if(returnType == Integer.class) {
			return Integer.valueOf(rawValue);
		}
		else if(returnType == Byte.class) {
			return Byte.valueOf(rawValue);
		}
		else if(returnType == Double.class) {
			return Double.valueOf(rawValue);
		}
		else if(returnType == Float.class) {
			return Float.valueOf(rawValue);
		}
		else if(returnType == Long.class) {
			return Long.valueOf(rawValue);
		}
		else if(returnType == Short.class) {
			return Short.valueOf(rawValue);
		}
		throw new UnsupportedDataTypeException("Cannot support this primitive type!");
	}
	
	private String[] trim(String[] values) {
		String[] ret = new String[values.length];
		for(int i = 0; i < ret.length; i++) {
			ret[i] = values[i].trim();
		}
		return ret;
	}
}
